4-1 高可用管理多个ConsulService实例
多 ConsulService 实例的需求
当 Gateway 需要与多个微服务通信时,每个微服务对应一个独立的 ConsulService 实例。问题是:当前的 forRoot 模式只能创建一个 ConsulService 实例。
Gateway
├── ConsulService (user-service) → gRPC Client A
├── ConsulService (order-service) → gRPC Client B
└── ConsulService (payment-service) → gRPC Client C
text
使用 Token 区分实例
NestJS 的依赖注入支持通过 Token 区分同一类型的不同实例:
forRoot 扩展参数
// consul.module.ts
@Module({})
export class ConsulModule {
static forRoot(options: ConsulModuleOptions & { token?: string }): DynamicModule {
const tokenName = options.token || 'CONSUL_SERVICE';
const consulServiceProvider: Provider = {
provide: tokenName,
useClass: ConsulService,
};
const optionsProvider: Provider = {
provide: 'CONSUL_OPTIONS',
useValue: options,
};
return {
module: ConsulModule,
providers: [consulServiceProvider, optionsProvider],
exports: [consulServiceProvider],
global: true,
};
}
}
typescript
AppModule 中注册多个实例
@Module({
imports: [
ConsulModule.forRoot({
token: 'USER_CONSUL_SERVICE',
serviceName: 'user-service',
host: 'consul-server',
port: 8500,
}),
// 后续可以注册更多服务
],
})
export class AppModule {}
typescript
在 Service 中通过 Token 注入
@Injectable()
export class AuthService {
constructor(
@Inject('USER_CONSUL_SERVICE') private consulService: ConsulService,
) {}
async findOne(id: string) {
const client = await this.consulService.getInstance();
// ...
}
}
typescript
拦截器的问题
使用 Token 注入后,拦截器面临新问题:
拦截器是通用的,不能为每个 ConsulService 实例各写一个拦截器。
问题:
拦截器如何知道当前请求应该使用哪个 ConsulService 实例?
方案一:装饰器标记
└── 在 Controller 方法上通过装饰器标注关联的微服务名
方案二:请求头传递
└── 前端在请求 Header 中标注需要的微服务名
方案三:中心化管理器(推荐)
└── 维护一个 Map<serviceName, ConsulService>
text
方案一:装饰器标记
定义装饰器
// 自定义装饰器,标注 Controller 方法关联的微服务
export const Microservice = (name: string) =>
SetMetadata('microservice', name);
typescript
使用装饰器
@Controller('users')
export class UserController {
@Get(':id')
@Microservice('user-service')
findOne(@Param('id') id: string) {
// ...
}
}
typescript
拦截器中读取
@Injectable()
export class GrpcExceptionInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const microservice = Reflector.get('microservice', context.getHandler());
// 根据 microservice 查找对应的 ConsulService
}
}
typescript
局限性
| 局限 | 说明 |
|---|---|
| 单一服务 | 一个方法只能关联一个微服务 |
| 多服务场景 | 需要支持一个方法关联多个微服务 |
| 代码侵入 | 需要在每个 Controller 方法上添加装饰器 |
方案三:中心化管理器
维护一个全局的 Map<serviceName, ConsulService>:
@Injectable()
export class ConsulServiceManager {
private services = new Map<string, ConsulService>();
register(name: string, service: ConsulService) {
this.services.set(name, service);
}
get(name: string): ConsulService | undefined {
return this.services.get(name);
}
}
typescript
拦截器通过装饰器或请求头获取服务名,从 Manager 中查找对应的 ConsulService。
参考资源
- NestJS Dependency Injection - 自定义 Provider
- NestJS Reflection - Reflector 使用
- NestJS Custom Decorators - 自定义装饰器
↑